/*
* Copyright 2000-2016 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.tests.components.grid.basicfeatures.escalator;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.Before;
import org.junit.ComparisonFailure;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebElement;
import com.vaadin.client.WidgetUtil;
import com.vaadin.shared.Range;
import com.vaadin.testbench.TestBenchElement;
import com.vaadin.testbench.elements.NotificationElement;
import com.vaadin.testbench.parallel.BrowserUtil;
import com.vaadin.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest;
@SuppressWarnings("boxing")
public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest {
//@formatter:off
// separate strings made so that eclipse can show the concatenated string by hovering the mouse over the constant
// translate3d(0px, 40px, 123px);
// translate3d(24px, 15.251px, 0);
// translate(0, 40px);
private final static String TRANSLATE_VALUE_REGEX =
"translate(?:3d|)" // "translate" or "translate3d"
+ "\\(" // literal "("
+ "(" // start capturing the x argument
+ "[0-9]+" // the integer part of the value
+ "(?:" // start of the subpixel part of the value
+ "\\.[0-9]" // if we have a period, there must be at least one number after it
+ "[0-9]*" // any amount of accuracy afterwards is fine
+ ")?" // the subpixel part is optional
+ ")"
+ "(?:px)?" // we don't care if the values are suffixed by "px" or not.
+ ", "
+ "(" // start capturing the y argument
+ "[0-9]+" // the integer part of the value
+ "(?:" // start of the subpixel part of the value
+ "\\.[0-9]" // if we have a period, there must be at least one number after it
+ "[0-9]*" // any amount of accuracy afterwards is fine
+ ")?" // the subpixel part is optional
+ ")"
+ "(?:px)?" // we don't care if the values are suffixed by "px" or not.
+ "(?:, .*?)?" // the possible z argument, uninteresting (translate doesn't have one, translate3d does)
+ "\\)" // literal ")"
+ ";?"; // optional ending semicolon
// 40px;
// 12.34px
private final static String PIXEL_VALUE_REGEX =
"(" // capture the pixel value
+ "[0-9]+" // the pixel argument
+ "(?:" // start of the subpixel part of the value
+ "\\.[0-9]" // if we have a period, there must be at least one number after it
+ "[0-9]*" // any amount of accuracy afterwards is fine
+ ")?" // the subpixel part is optional
+ ")"
+ "(?:px)?" // optional "px" string
+ ";?"; // optional semicolon
//@formatter:on
// also matches "-webkit-transform";
private final static Pattern TRANSFORM_CSS_PATTERN = Pattern
.compile("transform: (.*?);");
private final static Pattern TOP_CSS_PATTERN = Pattern.compile(
"top: ([0-9]+(?:\\.[0-9]+)?(?:px)?);?", Pattern.CASE_INSENSITIVE);
private final static Pattern LEFT_CSS_PATTERN = Pattern.compile(
"left: ([0-9]+(?:\\.[0-9]+)?(?:px)?);?", Pattern.CASE_INSENSITIVE);
private final static Pattern TRANSLATE_VALUE_PATTERN = Pattern
.compile(TRANSLATE_VALUE_REGEX);
private final static Pattern PIXEL_VALUE_PATTERN = Pattern
.compile(PIXEL_VALUE_REGEX, Pattern.CASE_INSENSITIVE);
@Before
public void before() {
setDebug(true);
openTestURL("theme=reindeer");
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, "Set 20px default height");
populate();
}
@Test
public void openVisibleSpacer() {
assertFalse("No spacers should be shown at the start",
spacersAreFoundInDom());
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
assertNotNull("Spacer should be shown after setting it", getSpacer(1));
}
@Test
public void closeVisibleSpacer() {
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_1, REMOVE);
assertNull("Spacer should not exist after removing it", getSpacer(1));
}
@Test
public void spacerPushesVisibleRowsDown() {
double oldTop = getElementTop(getBodyRow(2));
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
double newTop = getElementTop(getBodyRow(2));
assertGreater("Row below a spacer was not pushed down", newTop, oldTop);
}
@Test
public void addingRowAboveSpacerPushesItDown() {
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ALL_ROWS);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
double oldTop = getElementTop(getSpacer(1));
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);
double newTop = getElementTop(getSpacer(2));
assertGreater("Spacer should've been pushed down (oldTop: " + oldTop
+ ", newTop: " + newTop + ")", newTop, oldTop);
}
@Test
public void addingRowBelowSpacerDoesNotPushItDown() {
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ALL_ROWS);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
double oldTop = getElementTop(getSpacer(1));
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_END);
double newTop = getElementTop(getSpacer(1));
assertEquals("Spacer should've not been pushed down", newTop, oldTop,
WidgetUtil.PIXEL_EPSILON);
}
@Test
public void addingRowBelowSpacerIsActuallyRenderedBelowWhenEscalatorIsEmpty() {
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, REMOVE_ALL_ROWS);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_BEGINNING);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
double spacerTop = getElementTop(getSpacer(1));
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, ADD_ONE_ROW_TO_END);
double rowTop = getElementTop(getBodyRow(2));
assertEquals("Next row should've been rendered below the spacer",
spacerTop + 100, rowTop, WidgetUtil.PIXEL_EPSILON);
}
@Test
public void addSpacerAtBottomThenScrollThere() {
selectMenuPath(FEATURES, SPACERS, ROW_99, SET_100PX);
scrollVerticallyTo(999999);
assertFalse("Did not expect a notification",
$(NotificationElement.class).exists());
}
@Test
public void scrollToBottomThenAddSpacerThere() {
scrollVerticallyTo(999999);
long oldBottomScrollTop = getScrollTop();
selectMenuPath(FEATURES, SPACERS, ROW_99, SET_100PX);
assertEquals(
"Adding a spacer underneath the current viewport should "
+ "not scroll anywhere",
oldBottomScrollTop, getScrollTop());
assertFalse("Got an unexpected notification",
$(NotificationElement.class).exists());
scrollVerticallyTo(999999);
assertFalse("Got an unexpected notification",
$(NotificationElement.class).exists());
assertGreater("Adding a spacer should've made the scrollbar scroll "
+ "further", getScrollTop(), oldBottomScrollTop);
}
@Test
public void removingRowAboveSpacerMovesSpacerUp() {
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
WebElement spacer = getSpacer(1);
double originalElementTop = getElementTop(spacer);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS,
REMOVE_ONE_ROW_FROM_BEGINNING);
assertLessThan("spacer should've moved up", getElementTop(spacer),
originalElementTop);
assertNull("No spacer for row 1 should be found after removing the "
+ "top row", getSpacer(1));
}
@Test
public void removingSpacedRowRemovesSpacer() {
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
assertTrue("Spacer should've been found in the DOM",
spacersAreFoundInDom());
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS,
REMOVE_ONE_ROW_FROM_BEGINNING);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS,
REMOVE_ONE_ROW_FROM_BEGINNING);
assertFalse("No spacers should be in the DOM after removing "
+ "associated spacer", spacersAreFoundInDom());
}
@Test
public void spacersAreFixedInViewport_firstFreezeThenScroll() {
selectMenuPath(FEATURES, FROZEN_COLUMNS, FREEZE_1_COLUMN);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
assertEquals(
"Spacer's left position should've been 0 at the " + "beginning",
0d, getElementLeft(getSpacer(1)), WidgetUtil.PIXEL_EPSILON);
int scrollTo = 10;
scrollHorizontallyTo(scrollTo);
assertEquals(
"Spacer's left position should've been " + scrollTo
+ " after scrolling " + scrollTo + "px",
scrollTo, getElementLeft(getSpacer(1)),
WidgetUtil.PIXEL_EPSILON);
}
@Test
public void spacersAreFixedInViewport_firstScrollThenFreeze() {
selectMenuPath(FEATURES, FROZEN_COLUMNS, FREEZE_1_COLUMN);
int scrollTo = 10;
scrollHorizontallyTo(scrollTo);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
assertEquals(
"Spacer's left position should've been " + scrollTo
+ " after scrolling " + scrollTo + "px",
scrollTo, getElementLeft(getSpacer(1)),
WidgetUtil.PIXEL_EPSILON);
}
@Test
public void addingMinusOneSpacerDoesNotScrollWhenScrolledAtTop() {
scrollVerticallyTo(5);
selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, SET_100PX);
assertEquals(
"No scroll adjustment should've happened when adding the -1 spacer",
5, getScrollTop());
}
@Test
public void removingMinusOneSpacerScrolls() {
scrollVerticallyTo(5);
selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, REMOVE);
assertEquals("Scroll adjustment should've happened when removing the "
+ "-1 spacer", 0, getScrollTop());
}
@Test
public void scrollToRowWorksProperlyWithSpacers() throws Exception {
selectMenuPath(FEATURES, SPACERS, ROW_MINUS1, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
/*
* we check for row -3 instead of -1, because escalator has two rows
* buffered underneath the footer
*/
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_75);
Thread.sleep(500);
assertEquals("Row 75: 0,75", getBodyCell(-3, 0).getText());
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_25);
Thread.sleep(500);
try {
assertEquals("Row 25: 0,25", getBodyCell(0, 0).getText());
} catch (ComparisonFailure retryForIE10andIE11) {
/*
* This seems to be some kind of subpixel/off-by-one-pixel error.
* Everything's scrolled correctly, but Escalator still loads one
* row above to the DOM, underneath the header. It's there, but it's
* not visible. We'll allow for that one pixel error.
*/
assertEquals("Row 24: 0,24", getBodyCell(0, 0).getText());
}
}
@Test
public void scrollToSpacerFromAbove() throws Exception {
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING);
// Browsers might vary with a few pixels.
Range allowableScrollRange = Range.between(765, 780);
int scrollTop = (int) getScrollTop();
assertTrue("Scroll position was not " + allowableScrollRange + ", but "
+ scrollTop, allowableScrollRange.contains(scrollTop));
}
@Test
public void scrollToSpacerFromBelow() throws Exception {
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
scrollVerticallyTo(999999);
selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING);
// Browsers might vary with a few pixels.
Range allowableScrollRange = Range.between(1015, 1025);
int scrollTop = (int) getScrollTop();
assertTrue("Scroll position was not " + allowableScrollRange + ", but "
+ scrollTop, allowableScrollRange.contains(scrollTop));
}
@Test
public void scrollToSpacerAlreadyInViewport() throws Exception {
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
scrollVerticallyTo(1000);
selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING);
assertEquals(getScrollTop(), 1000);
}
@Test
public void scrollToRowAndSpacerFromAbove() throws Exception {
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_50,
SCROLL_HERE_SPACERBELOW_ANY_0PADDING);
// Browsers might vary with a few pixels.
Range allowableScrollRange = Range.between(765, 780);
int scrollTop = (int) getScrollTop();
assertTrue("Scroll position was not " + allowableScrollRange + ", but "
+ scrollTop, allowableScrollRange.contains(scrollTop));
}
@Test
public void scrollToRowAndSpacerFromBelow() throws Exception {
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
scrollVerticallyTo(999999);
selectMenuPath(FEATURES, SPACERS, ROW_50,
SCROLL_HERE_SPACERBELOW_ANY_0PADDING);
// Browsers might vary with a few pixels.
Range allowableScrollRange = Range.between(995, 1005);
int scrollTop = (int) getScrollTop();
assertTrue("Scroll position was not " + allowableScrollRange + ", but "
+ scrollTop, allowableScrollRange.contains(scrollTop));
}
@Test
public void scrollToRowAndSpacerAlreadyInViewport() throws Exception {
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
scrollVerticallyTo(950);
selectMenuPath(FEATURES, SPACERS, ROW_50,
SCROLL_HERE_SPACERBELOW_ANY_0PADDING);
assertEquals(getScrollTop(), 950);
}
@Test
public void domCanBeSortedWithFocusInSpacer() throws InterruptedException {
// Firefox behaves badly with focus-related tests - skip it.
if (BrowserUtil.isFirefox(super.getDesiredCapabilities())) {
return;
}
selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
WebElement inputElement = getEscalator()
.findElement(By.tagName("input"));
inputElement.click();
scrollVerticallyTo(30);
// Sleep needed because of all the JS we're doing, and to let
// the DOM reordering to take place.
Thread.sleep(500);
assertFalse("Error message detected",
$(NotificationElement.class).exists());
}
@Test
public void spacersAreInsertedInCorrectDomPosition() {
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
WebElement tbody = getEscalator().findElement(By.tagName("tbody"));
WebElement spacer = getChild(tbody, 2);
String cssClass = spacer.getAttribute("class");
assertTrue(
"element index 2 was not a spacer (class=\"" + cssClass + "\")",
cssClass.contains("-spacer"));
}
@Test
public void spacersAreInCorrectDomPositionAfterScroll() {
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
scrollVerticallyTo(32); // roughly one row's worth
WebElement tbody = getEscalator().findElement(By.tagName("tbody"));
WebElement spacer = getChild(tbody, 1);
String cssClass = spacer.getAttribute("class");
assertTrue(
"element index 1 was not a spacer (class=\"" + cssClass + "\")",
cssClass.contains("-spacer"));
}
@Test
public void spacerScrolledIntoViewGetsFocus() {
selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER);
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING);
tryToTabIntoFocusUpdaterElement();
assertEquals("input", getFocusedElement().getTagName());
}
@Test
public void spacerScrolledOutOfViewDoesNotGetFocus() {
selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING);
tryToTabIntoFocusUpdaterElement();
assertNotEquals("input", getFocusedElement().getTagName());
}
@Test
public void spacerOpenedInViewGetsFocus() {
selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
tryToTabIntoFocusUpdaterElement();
WebElement focusedElement = getFocusedElement();
assertEquals("input", focusedElement.getTagName());
}
@Test
public void spacerOpenedOutOfViewDoesNotGetFocus() {
selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER);
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
tryToTabIntoFocusUpdaterElement();
assertNotEquals("input", getFocusedElement().getTagName());
}
@Test
public void spacerOpenedInViewAndScrolledOutAndBackAgainGetsFocus() {
selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER);
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_50);
selectMenuPath(FEATURES, SPACERS, ROW_1, SCROLL_HERE_ANY_0PADDING);
tryToTabIntoFocusUpdaterElement();
assertEquals("input", getFocusedElement().getTagName());
}
@Test
public void spacerOpenedOutOfViewAndScrolledInAndBackAgainDoesNotGetFocus() {
selectMenuPath(FEATURES, SPACERS, FOCUSABLE_UPDATER);
selectMenuPath(FEATURES, SPACERS, ROW_50, SET_100PX);
selectMenuPath(FEATURES, SPACERS, ROW_50, SCROLL_HERE_ANY_0PADDING);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_0);
tryToTabIntoFocusUpdaterElement();
assertNotEquals("input", getFocusedElement().getTagName());
}
private void tryToTabIntoFocusUpdaterElement() {
((TestBenchElement) findElement(By.className("gwt-MenuBar"))).focus();
WebElement focusedElement = getFocusedElement();
focusedElement.sendKeys(Keys.TAB);
}
private WebElement getChild(WebElement parent, int childIndex) {
return (WebElement) executeScript(
"return arguments[0].children[" + childIndex + "];", parent);
}
private static double[] getElementDimensions(WebElement element) {
/*
* we need to parse the style attribute, since using getCssValue gets a
* normalized value that is harder to parse.
*/
String style = element.getAttribute("style");
String transform = getTransformFromStyle(style);
if (transform != null) {
return getTranslateValues(transform);
}
double[] result = new double[] { -1, -1 };
String left = getLeftFromStyle(style);
if (left != null) {
result[0] = getPixelValue(left);
}
String top = getTopFromStyle(style);
if (top != null) {
result[1] = getPixelValue(top);
}
if (result[0] != -1 && result[1] != -1) {
return result;
} else {
throw new IllegalArgumentException("Could not parse the position "
+ "information from the CSS \"" + style + "\"");
}
}
private static double getElementTop(WebElement element) {
return getElementDimensions(element)[1];
}
private static double getElementLeft(WebElement element) {
return getElementDimensions(element)[0];
}
private static String getTransformFromStyle(String style) {
return getFromStyle(TRANSFORM_CSS_PATTERN, style);
}
private static String getTopFromStyle(String style) {
return getFromStyle(TOP_CSS_PATTERN, style);
}
private static String getLeftFromStyle(String style) {
return getFromStyle(LEFT_CSS_PATTERN, style);
}
private static String getFromStyle(Pattern pattern, String style) {
Matcher matcher = pattern.matcher(style);
if (matcher.find()) {
assertEquals("wrong amount of groups matched in " + style, 1,
matcher.groupCount());
return matcher.group(1);
} else {
return null;
}
}
/**
* @return {@code [0] == x}, {@code [1] == y}
*/
private static double[] getTranslateValues(String translate) {
Matcher matcher = TRANSLATE_VALUE_PATTERN.matcher(translate);
assertTrue("no matches for " + translate + " against "
+ TRANSLATE_VALUE_PATTERN, matcher.find());
assertEquals("wrong amout of groups matched in " + translate, 2,
matcher.groupCount());
return new double[] { Double.parseDouble(matcher.group(1)),
Double.parseDouble(matcher.group(2)) };
}
private static double getPixelValue(String top) {
Matcher matcher = PIXEL_VALUE_PATTERN.matcher(top);
assertTrue(
"no matches for \"" + top + "\" against " + PIXEL_VALUE_PATTERN,
matcher.find());
assertEquals("wrong amount of groups matched in " + top, 1,
matcher.groupCount());
return Double.parseDouble(matcher.group(1));
}
}